依照【Python錦囊㊙️技10】OOA、OOD and OOP討論的軟體開發生命週期(SDLC),程式撰寫完後,必須進行單元測試(Unit Testing),確保程式正確且符合設計規格。
圖一. 瀑布型模型(Waterfall Model)
通常單元測試有幾項要求:
Python內建測試模組unittest,也有一個知名的套件PyTest,另外,有些開發框架(Framework)套件也有自己的測試模組,例如Django,可參閱【Testing in Django】。本文主要介紹Python內建模組unittest及pytest。
單元測試有幾個專有名詞說明如下:
unittest以OOP方式程式開發,建立類別繼承unittest.TestCase,類別裡面的每個函數代表一個測試案例。
範例1. 簡單測試,程式來自【官方unittest文件】,程式名稱為17\test1.py。
import unittest
# 測試類別,繼承unittest.TestCase
class TestStringMethods(unittest.TestCase):
# 測試案例1:大寫
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
if __name__ == '__main__':
# 執行測試
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
# 測試案例2:測試 isupper 方法
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
# 測試案例3:測試分詞(split)
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
s = 'helloxworld'
.F.
======================================================================
FAIL: test_split (__main__.TestStringMethods.test_split)
----------------------------------------------------------------------
Traceback (most recent call last):
File "F:\0_python\00_MY\0_ITHome\src\17\test1.py", line 17, in test_split
self.assertEqual(s.split(), ['hello', 'world'])
AssertionError: Lists differ: ['helloxworld'] != ['hello', 'world']
First differing element 0:
'helloxworld'
'hello'
Second list contains 1 additional elements.
First extra element 1:
'world'
- ['helloxworld']
? ^
+ ['hello', 'world']
? ^^^^
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s
OK
範例2. 通常受測程式與測試程式會分開,甚至測試程式會儲存在另一個資料夾,通常會命名為tests,程式放在17\project1資料夾,規劃如下:
# 階層(factorial )計算
from functools import reduce
def factorial(n):
return reduce(lambda x, y: x * y, range(1, n+1))
import unittest
# 引用 main_program 資料夾的factorial函數
from main_program import factorial
class TestMath(unittest.TestCase):
def test_factorial(self):
# 測試階層計算是否正確
self.assertEqual(factorial(5), 1*2*3*4*5)
if __name__ == '__main__':
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
import unittest, sys, os
# 將路徑加入環境變數
sys.path.append(os.path.abspath(".."))
除了assertEqual,unittest還有許多Assert指令:
圖一. Assert指令列表,圖片來源:【Getting Started With Testing in Python】
測試前有時候要準備資料或資料庫/網路連線,可在setUp函數內實現,另外tearDown函數可關閉資源,例如資料庫/網路連線。
範例3. 在setUp函數準備資料,程式名稱為17\test_fixture.py。
# 檢查是否為質數
def check_prime(i):
for j in range(2, math.floor(i/2)+1):
if i % j == 0:
return False
return True
import unittest
import math
# 檢查是否為質數
def check_prime(i):
for j in range(2, math.floor(i/2)+1):
if i % j == 0:
return False
return True
# 測試類別,繼承unittest.TestCase
class TestPrime(unittest.TestCase):
def setUp(self):
self.prime_number = [2,3,5,7,11]
self.non_prime_number = [4,9,15,30]
# 測試案例1:測試 prime
def test_prime(self):
for i in self.prime_number:
self.assertEqual(check_prime(i), True)
# 測試案例2:測試 non prime
def test_non_prime(self):
for i in self.non_prime_number:
self.assertEqual(check_prime(i), False)
if __name__ == '__main__':
# 執行測試
unittest.main()
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
單元測試是程式設計師的當責工作,在交給測試部門進行整合測試前,必須確保每支程式的品質,避免在團隊合併程式測試時,造成處處暴雷的狀況,後果不可收拾。下一篇我們會繼續討論更多的測試技巧。
本系列的程式碼會統一放在GitHub,本篇的程式放在src/17資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。